{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../common/rfsoc_book_banner.jpg\" alt=\"University of Strathclyde\" align=\"left\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Notebook Set B\n",
    "\n",
    "----\n",
    "\n",
    "## 01 - Sampling\n",
    "This notebook motivates the need to sample a signal appropriately to capture all of its information. We will begin by introducing the sampling process and explore sample rates used by different applications. Then we will investigate the effect of sampling a \"continuous\" waveform at different sampling frequencies and observe the effects of slow, fast, and reasonable sampling. Finally, the Nyquist sampling theorem will be introduced and the effects of aliasing discussed.\n",
    "\n",
    "## Table of Contents\n",
    "* [1. Introduction](#introduction)\n",
    "* [2. Sampling - How Fast?](#sampling_how_fast)\n",
    "* [3. Sampling Theorem](#sampling_theorem)\n",
    "* [4. Aliasing](#aliasing)\n",
    "* [5. Conclusion](#conclusion)\n",
    "\n",
    "## Revision\n",
    "* **v1.0** | 21/12/22 | _First Revision_\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Introduction <a class=\"anchor\" id=\"introduction\"></a>\n",
    "Sampling is traditionally performed by an analogue to digital converter (ADC) and can be thought of as recording the voltage level of an analogue signal at an instant in time. This sample is assigned a binary number through a process called quantisation - more on that later. By collecting samples at a regular period, we can approximate an analogue signal in a digital system. An illustration showing the discrete sampling process is presented in Figure 1.\n",
    "\n",
    "<figure>\n",
    "<img src=\"./images/Analogue_Digital_Signals.png\" style=\"width: 80%;\"/>\n",
    "    <figcaption><b>Figure 1: The discrete sampling process.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "The time between samples is called the sampling period, $t_{s}$. The speed at which an ADC generates binary numbers is called the sampling rate or sampling frequency, $f_{s}$.\n",
    "\n",
    "$$\n",
    "f_{s} = 1 / t_{s}\n",
    "$$\n",
    "\n",
    "Sampling frequency is quoted in samples per second, or simply as Hertz (Hz). \n",
    "\n",
    "Let us begin by importing relevant Python libraries and modules for this notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "from scipy import signal\n",
    "import numpy as np\n",
    "import scipy.fftpack\n",
    "from rfsoc_book import helper_functions as hf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This notebook is written for, and runs on, a digital system. Therefore, we cannot realistically generate a continuous analogue signal. However, for the purposes of this demonstration, we can approximate a continuous signal by creating a one using a high sampling rate.\n",
    "\n",
    "In the following cell, we will create a \"continuous\" sine wave of frequency 100 Hz. The formula for generating a sine wave is\n",
    "\n",
    "$$\n",
    "y(t) = Asin(2\\pi ft + \\phi)\n",
    "$$\n",
    "\n",
    "where $\\phi$ is the phase in radians, $f$ is the waveform's frequency, $t$ is the time, and $A$ is the waveform's amplitude."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "A = 1       # Amplitude\n",
    "f = 100     # Desired frequency \n",
    "t = 0.06    # Time\n",
    "p = 10      # Phase\n",
    "\n",
    "ar = 48000  # A high sample rate to approximate a continuous signal. ar = analogue rate\n",
    "\n",
    "x_cont = np.arange(0, t, 1/ar)                    # Discrete time i.e. sampled time period\n",
    "y_cont = A * np.sin(2 * np.pi * f * x_cont + p)   # Formula for sine wave\n",
    "\n",
    "ax = hf.plot_timeseries(\"'Continuous' Waveform\",\n",
    "                        [x_cont], [y_cont],\n",
    "                        ['continuous'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Sampling - How Fast? <a class=\"anchor\" id=\"sampling_how_fast\"></a>\n",
    "\n",
    "We must consider the required **sampling rate** to use so that we can retain **all** of the information in a signal.\n",
    "\n",
    "Below, we will sample the \"continuous\" 100 Hz signal at various sampling rates and inspect the results.\n",
    "\n",
    "Sampling at $f_{s}$=100 Hz, which is equivalent to 1 sample per period, gives the following waveform."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "A = 1       # Amplitude\n",
    "f = 100     # Desired frequency \n",
    "t = 0.06    # Time\n",
    "p = 10      # Phase\n",
    "\n",
    "fs = 100    # 100 Hz (1 sample per period)\n",
    "\n",
    "x = np.arange(0, t, 1/fs)                # Discrete time i.e. sampled time period\n",
    "y = A * np.sin(2 * np.pi * f * x + p)    # Formula for sine wave\n",
    "\n",
    "hf.plot_timeseries(\"Sampling Too Slow\",\n",
    "                   [x_cont, x], [y_cont, y], \n",
    "                   ['continuous', 'discrete'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The signal here is interpreted as DC with an offset of -0.54 V. \n",
    "\n",
    "Sampling at $f_{s}$=80 Hz, which is 1 sample every 0.8 of a period, produces the waveform below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "A = 1       # Amplitude\n",
    "f = 100     # Desired frequency \n",
    "t = 0.06    # Time\n",
    "p = 10      # Phase\n",
    "\n",
    "fs = 80    # 80 Hz (1 sample every 0.8 of a period)\n",
    "\n",
    "x = np.arange(0, t, 1/fs)                # Discrete time i.e. sampled time period\n",
    "y = A * np.sin(2 * np.pi * f * x + p)    # Formula for sine wave\n",
    "\n",
    "hf.plot_timeseries(\"Sampling Too Slow\",\n",
    "                   [x_cont, x], [y_cont, y],\n",
    "                   ['continuous', 'discrete'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Most of the signal's features are \"missed\". The sampling rate is too slow to record enough of the information to accurately reconstruct the signal. When the sampling rate is too slow, a sampled signal can be interpreted as having a lower frequency. \n",
    "\n",
    "Sampling at $f_{s}$=800 Hz, which is 8 samples per period, results in the following waveform."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "A = 1       # Amplitude\n",
    "f = 100     # Desired frequency \n",
    "t = 0.06    # Time\n",
    "p = 10      # Phase\n",
    "\n",
    "fs = 800    # 800 Hz (8 samples per period)\n",
    "\n",
    "x = np.arange(0, t, 1/fs)                # Discrete time i.e. sampled time period\n",
    "y = A * np.sin(2 * np.pi * f * x + p)    # Formula for sine wave\n",
    "\n",
    "hf.plot_timeseries(\"Sampling Too Fast\", \n",
    "                   [x_cont, x], [y_cont, y], \n",
    "                   ['continuous', 'discrete'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This sample rate appears to be \"reasonable\".\n",
    "\n",
    "Finally, sampling at $f_{s}$=3000 Hz, which is equivalent to 30 samples per period, gives us the waveform below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "A = 1       # Amplitude\n",
    "f = 100     # Desired frequency \n",
    "t = 0.06    # Time\n",
    "p = 10      # Phase\n",
    "\n",
    "fs = 3000    # 3000 Hz (30 samples per period)\n",
    "\n",
    "x = np.arange(0, t, 1/fs)                # Discrete time i.e. sampled time period\n",
    "y = A * np.sin(2 * np.pi * f * x + p)    # Formula for sine wave\n",
    "\n",
    "hf.plot_timeseries(\"Sampling Too Fast\", \n",
    "                   [x_cont, x], [y_cont, y], \n",
    "                   ['continuous', 'discrete'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Perhaps this rate is higher than necessary.\n",
    "\n",
    "Sampling too high will improve the approximation of the original signal. However, a higher sample frequency will require more processing.\n",
    "\n",
    "What is a \"suitable\" sampling rate?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Sampling Theorem <a class=\"anchor\" id=\"sampling_theorem\"></a>\n",
    "\n",
    "From mathematical theory, the minimum sampling rate required to retain all information is referred to as the **Nyquist frequency/rate**, which is denoted as $f_n$. The Nyquist frequency is defined as,\n",
    "\n",
    "$$\n",
    "f_{n}\\ge 2 f_{max}\n",
    "$$\n",
    "\n",
    "where $f_{max}$ is the maximum frequency component of a **baseband** or **bandlimited** signal.\n",
    "\n",
    "* A baseband signal consists of a set of frequencies that are not modulated. Typically, these frequencies are around 0Hz.\n",
    "* A bandlimited signal contains frequencies in the range $f_{l}<f<f_h$, where $f_{l}$ is the lowest frequency and $f_h$ is the highest frequency.\n",
    "\n",
    "An example frequency spectra of a baseband signal and a bandlimited signal is presented in Figure 2.\n",
    "\n",
    "<figure>\n",
    "<img src=\"./images/baseband_bandpass.png\" style=\"width: 75%; vertical-align: middle;\"/>\n",
    "    <figcaption><b>Figure 2: Frequency spectra of a baseband signal.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "Notice that in each of the above cases, it is necessary to sample twice as fast as the highest frequency component in the signal to acquire all of its information."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Aliasing <a class=\"anchor\" id=\"aliasing\"></a>\n",
    "When a (baseband) signal is sampled at a frequency below the Nyquist frequency, then we “lose” the signal's information and aliasing is said to have occurred. \n",
    "\n",
    "Aliasing can be illustrated by sampling a sine wave below the Nyquist frequency. The sine wave will appear to have a lower frequency.\n",
    "\n",
    "Consider again, sampling the 100 Hz size wave at 80 Hz."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "A = 1       # Amplitude\n",
    "f = 100     # Desired frequency \n",
    "t = 0.2     # Time\n",
    "p = 10      # Phase\n",
    "\n",
    "fs = 80     # Sampling frequency \n",
    " \n",
    "# 'Continuous' waveform\n",
    "x_cont = np.arange(0, t, 1/ar)                   \n",
    "y_cont = A * np.sin(2 * np.pi * f * x_cont + p)   \n",
    "\n",
    "# Sampled waveform\n",
    "x = np.arange(0, t, 1/fs)               \n",
    "y = A * np.sin(2 * np.pi * f * x + p)   \n",
    "\n",
    "# Perceived aliased waveform\n",
    "f_alias = f-fs*np.round(f/fs)\n",
    "x_alias = np.arange(0, t, 1/ar)\n",
    "y_alias = A * np.sin(2 * np.pi * f_alias * x_alias + p)\n",
    "\n",
    "hf.plot_timeseries(\"Aliasing\",\n",
    "                   [x_cont, x, x_alias], [y_cont, y, y_alias],\n",
    "                   ['continuous', 'discrete', 'dash'])\n",
    "print(\"Perceived frequency: \", f_alias, \"Hz\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Observe that when the sampling rate does not obey the sampling theorem stated above, the perceived waveform is of a lower frequency. In the case of a desired frequency of 100 Hz and a sampling rate of 80 Hz, the perceived signal has a frequency of \n",
    "\n",
    "$$\n",
    "f_{s}-f_{signal} = 100Hz - 80Hz = 20 Hz.\n",
    "$$\n",
    "\n",
    "<div class=\"alert alert-box alert-info\">\n",
    "Try changing the sampling rate and/or desired frequency above and observe the effects on the perceived waveform.\n",
    "</div>\n",
    "\n",
    "To better illustrate this concept, we can inspect the frequency domain by running the code cells below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = 0.5\n",
    "fs = 1000\n",
    "\n",
    "x_cont = np.arange(0, t, 1/ar) # 'Continuous'\n",
    "x = np.arange(0, t, 1/fs)      # Sampled\n",
    "\n",
    "A1 = 100\n",
    "A2 = 1\n",
    "A3 = 0.01\n",
    "\n",
    "# Case 1\n",
    "y1_cont = A1 * np.sin(2 * np.pi * (fs*0.1) * x_cont) \\\n",
    "        + A2 * np.sin(2 * np.pi * (fs*0.25) * x_cont) \\\n",
    "        + A3 * np.sin(2 * np.pi * (fs*0.4) * x_cont)             \n",
    "y1 = A1 * np.sin(2 * np.pi * (fs*0.1) * x) \\\n",
    "   + A2 * np.sin(2 * np.pi * (fs*0.25) * x) \\\n",
    "   + A3 * np.sin(2 * np.pi * (fs*0.4) * x)\n",
    "\n",
    "# Case 2\n",
    "y2_cont = A1 * np.sin(2 * np.pi * (fs*0.9) * x_cont) \\\n",
    "        + A2 * np.sin(2 * np.pi * (fs*0.75) * x_cont) \\\n",
    "        + A3 * np.sin(2 * np.pi * (fs*0.6) * x_cont)\n",
    "y2 = A1 * np.sin(2 * np.pi * (fs*0.9) * x) \\\n",
    "   + A2 * np.sin(2 * np.pi * (fs*0.75) * x) \\\n",
    "   + A3 * np.sin(2 * np.pi * (fs*0.6) * x)\n",
    "\n",
    "# Case 3\n",
    "y3_cont = A1 * np.sin(2 * np.pi * (fs*1.1) * x_cont) \\\n",
    "        + A2 * np.sin(2 * np.pi * (fs*1.25) * x_cont) \\\n",
    "        + A3 * np.sin(2 * np.pi * (fs*1.4) * x_cont)\n",
    "y3 = A1 * np.sin(2 * np.pi * (fs*1.1) * x) \\\n",
    "   + A2 * np.sin(2 * np.pi * (fs*1.25) * x) \\\n",
    "   + A3 * np.sin(2 * np.pi * (fs*1.4) * x)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "freqs_cont = np.fft.fftshift(np.fft.fftfreq(len(x_cont),1/ar)) #frequency domain axis for graphs\n",
    "freqs = np.fft.fftshift(np.fft.fftfreq(len(x),1/fs)) #frequency domain axis for graphs\n",
    "\n",
    "title = \"Case 1 - No Aliasing\"\n",
    "fft_cont = hf.find_fft(y1_cont,len(x_cont))\n",
    "fft_signal = hf.find_fft(y1,len(x))\n",
    "hf.plot_fft([freqs_cont, freqs], [fft_cont, fft_signal], \n",
    "         fs, title, label=['signal spectrum', 'signal spectrum'])\n",
    "\n",
    "title = \"Case 2 - Aliasing\"\n",
    "fft_cont = hf.find_fft(y2_cont,len(x_cont))\n",
    "fft_signal = hf.find_fft(y2,len(x))\n",
    "hf.plot_fft([freqs_cont, freqs], [fft_cont, fft_signal], \n",
    "         fs, title, label=['signal spectrum', 'aliased spectrum'])\n",
    "\n",
    "title = \"Case 3 - Aliasing\"\n",
    "fft_cont = hf.find_fft(y3_cont,len(x_cont))\n",
    "fft_signal = hf.find_fft(y3,len(x))\n",
    "hf.plot_fft([freqs_cont, freqs], [fft_cont, fft_signal],\n",
    "         fs, title, label=['signal spectrum', 'aliased spectrum'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* **Case 1 - No Aliasing :** The original signal contains frequencies that are less than $f_s/2$. Therefore, the signal is acquired without losing information using the first Nyquist zone.\n",
    "\n",
    "* **Case 2 - Aliasing :** The original signal contains frequencies higher than $f_s/2$, but less than $f_s$, which means the frequency components reside in the second Nyquist zone. This condition causes the signal's frequency spectra to fold across $f_s/2$. The frequency components are now considered to be of lower frequency as the original signal has been undersampled.\n",
    "\n",
    "* **Case 3 - Aliasing :** The original signal now contains frequencies higher than $f_s$, but less than $3f_s/2$, which means the frequency components reside in the third Nyquist zone. This condition causes the signal's frequency spectra to appear at lower frequencies below $f_s/2$ due to undersampling the original signal."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Conclusion <a class=\"anchor\" id=\"conclusion\"></a>\n",
    "This notebook explored the effects of discrete sampling. In the next notebook, we will investigate the quantisation process and effects of discretely acquiring an analogue signal in both time and amplitude."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "[⬅️ Previous Notebook](../notebook_A/04_overlays.ipynb) || [Next Notebook 🚀](02_quantisation.ipynb)\n",
    "\n",
    "Copyright © 2023 Strathclyde Academic Media\n",
    "\n",
    "---\n",
    "---"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
